{
 "cells": [
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:40:54.183641Z",
     "start_time": "2025-01-17T13:40:54.175835Z"
    }
   },
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 1.26.4\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.0\n",
      "torch 2.5.1+cpu\n",
      "cpu\n"
     ]
    }
   ],
   "execution_count": 10
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 准备数据"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:40:54.257879Z",
     "start_time": "2025-01-17T13:40:54.247274Z"
    }
   },
   "source": [
    "from sklearn.datasets import fetch_california_housing\n",
    "\n",
    "housing = fetch_california_housing()\n",
    "print(housing.DESCR)\n",
    "print(housing.data.shape)\n",
    "print(housing.target.shape)"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ".. _california_housing_dataset:\n",
      "\n",
      "California Housing dataset\n",
      "--------------------------\n",
      "\n",
      "**Data Set Characteristics:**\n",
      "\n",
      ":Number of Instances: 20640\n",
      "\n",
      ":Number of Attributes: 8 numeric, predictive attributes and the target\n",
      "\n",
      ":Attribute Information:\n",
      "    - MedInc        median income in block group\n",
      "    - HouseAge      median house age in block group\n",
      "    - AveRooms      average number of rooms per household\n",
      "    - AveBedrms     average number of bedrooms per household\n",
      "    - Population    block group population\n",
      "    - AveOccup      average number of household members\n",
      "    - Latitude      block group latitude\n",
      "    - Longitude     block group longitude\n",
      "\n",
      ":Missing Attribute Values: None\n",
      "\n",
      "This dataset was obtained from the StatLib repository.\n",
      "https://www.dcc.fc.up.pt/~ltorgo/Regression/cal_housing.html\n",
      "\n",
      "The target variable is the median house value for California districts,\n",
      "expressed in hundreds of thousands of dollars ($100,000).\n",
      "\n",
      "This dataset was derived from the 1990 U.S. census, using one row per census\n",
      "block group. A block group is the smallest geographical unit for which the U.S.\n",
      "Census Bureau publishes sample data (a block group typically has a population\n",
      "of 600 to 3,000 people).\n",
      "\n",
      "A household is a group of people residing within a home. Since the average\n",
      "number of rooms and bedrooms in this dataset are provided per household, these\n",
      "columns may take surprisingly large values for block groups with few households\n",
      "and many empty houses, such as vacation resorts.\n",
      "\n",
      "It can be downloaded/loaded using the\n",
      ":func:`sklearn.datasets.fetch_california_housing` function.\n",
      "\n",
      ".. rubric:: References\n",
      "\n",
      "- Pace, R. Kelley and Ronald Barry, Sparse Spatial Autoregressions,\n",
      "  Statistics and Probability Letters, 33 (1997) 291-297\n",
      "\n",
      "(20640, 8)\n",
      "(20640,)\n"
     ]
    }
   ],
   "execution_count": 11
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:40:54.264077Z",
     "start_time": "2025-01-17T13:40:54.258882Z"
    }
   },
   "source": [
    "# print(housing.data[0:5])\n",
    "import pprint  #打印的格式比较 好看\n",
    "\n",
    "pprint.pprint(housing.data[0:5])\n",
    "print('-'*50)\n",
    "pprint.pprint(housing.target[0:5])"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([[ 8.32520000e+00,  4.10000000e+01,  6.98412698e+00,\n",
      "         1.02380952e+00,  3.22000000e+02,  2.55555556e+00,\n",
      "         3.78800000e+01, -1.22230000e+02],\n",
      "       [ 8.30140000e+00,  2.10000000e+01,  6.23813708e+00,\n",
      "         9.71880492e-01,  2.40100000e+03,  2.10984183e+00,\n",
      "         3.78600000e+01, -1.22220000e+02],\n",
      "       [ 7.25740000e+00,  5.20000000e+01,  8.28813559e+00,\n",
      "         1.07344633e+00,  4.96000000e+02,  2.80225989e+00,\n",
      "         3.78500000e+01, -1.22240000e+02],\n",
      "       [ 5.64310000e+00,  5.20000000e+01,  5.81735160e+00,\n",
      "         1.07305936e+00,  5.58000000e+02,  2.54794521e+00,\n",
      "         3.78500000e+01, -1.22250000e+02],\n",
      "       [ 3.84620000e+00,  5.20000000e+01,  6.28185328e+00,\n",
      "         1.08108108e+00,  5.65000000e+02,  2.18146718e+00,\n",
      "         3.78500000e+01, -1.22250000e+02]])\n",
      "--------------------------------------------------\n",
      "array([4.526, 3.585, 3.521, 3.413, 3.422])\n"
     ]
    }
   ],
   "execution_count": 12
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:40:54.273076Z",
     "start_time": "2025-01-17T13:40:54.265080Z"
    }
   },
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "#拆分训练集和测试集，random_state是随机种子,同样的随机数种子，是为了得到同样的随机值\n",
    "x_train_all, x_test, y_train_all, y_test = train_test_split(\n",
    "    housing.data, housing.target, random_state = 7)\n",
    "x_train, x_valid, y_train, y_valid = train_test_split(\n",
    "    x_train_all, y_train_all, random_state = 11)\n",
    "# 训练集\n",
    "print(x_train.shape, y_train.shape)\n",
    "# 验证集\n",
    "print(x_valid.shape, y_valid.shape)\n",
    "# 测试集\n",
    "print(x_test.shape, y_test.shape)\n",
    "\n",
    "dataset_maps = {\n",
    "    \"train\": [x_train, y_train],\n",
    "    \"valid\": [x_valid, y_valid],\n",
    "    \"test\": [x_test, y_test],\n",
    "}\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(11610, 8) (11610,)\n",
      "(3870, 8) (3870,)\n",
      "(5160, 8) (5160,)\n"
     ]
    }
   ],
   "execution_count": 13
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:40:54.281090Z",
     "start_time": "2025-01-17T13:40:54.274083Z"
    }
   },
   "source": [
    "from sklearn.preprocessing import StandardScaler\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "scaler = StandardScaler()\n",
    "scaler.fit(x_train)"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "StandardScaler()"
      ],
      "text/html": [
       "<style>#sk-container-id-2 {\n",
       "  /* Definition of color scheme common for light and dark mode */\n",
       "  --sklearn-color-text: #000;\n",
       "  --sklearn-color-text-muted: #666;\n",
       "  --sklearn-color-line: gray;\n",
       "  /* Definition of color scheme for unfitted estimators */\n",
       "  --sklearn-color-unfitted-level-0: #fff5e6;\n",
       "  --sklearn-color-unfitted-level-1: #f6e4d2;\n",
       "  --sklearn-color-unfitted-level-2: #ffe0b3;\n",
       "  --sklearn-color-unfitted-level-3: chocolate;\n",
       "  /* Definition of color scheme for fitted estimators */\n",
       "  --sklearn-color-fitted-level-0: #f0f8ff;\n",
       "  --sklearn-color-fitted-level-1: #d4ebff;\n",
       "  --sklearn-color-fitted-level-2: #b3dbfd;\n",
       "  --sklearn-color-fitted-level-3: cornflowerblue;\n",
       "\n",
       "  /* Specific color for light theme */\n",
       "  --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, white)));\n",
       "  --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-icon: #696969;\n",
       "\n",
       "  @media (prefers-color-scheme: dark) {\n",
       "    /* Redefinition of color scheme for dark theme */\n",
       "    --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, #111)));\n",
       "    --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-icon: #878787;\n",
       "  }\n",
       "}\n",
       "\n",
       "#sk-container-id-2 {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 pre {\n",
       "  padding: 0;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 input.sk-hidden--visually {\n",
       "  border: 0;\n",
       "  clip: rect(1px 1px 1px 1px);\n",
       "  clip: rect(1px, 1px, 1px, 1px);\n",
       "  height: 1px;\n",
       "  margin: -1px;\n",
       "  overflow: hidden;\n",
       "  padding: 0;\n",
       "  position: absolute;\n",
       "  width: 1px;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-dashed-wrapped {\n",
       "  border: 1px dashed var(--sklearn-color-line);\n",
       "  margin: 0 0.4em 0.5em 0.4em;\n",
       "  box-sizing: border-box;\n",
       "  padding-bottom: 0.4em;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-container {\n",
       "  /* jupyter's `normalize.less` sets `[hidden] { display: none; }`\n",
       "     but bootstrap.min.css set `[hidden] { display: none !important; }`\n",
       "     so we also need the `!important` here to be able to override the\n",
       "     default hidden behavior on the sphinx rendered scikit-learn.org.\n",
       "     See: https://github.com/scikit-learn/scikit-learn/issues/21755 */\n",
       "  display: inline-block !important;\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-text-repr-fallback {\n",
       "  display: none;\n",
       "}\n",
       "\n",
       "div.sk-parallel-item,\n",
       "div.sk-serial,\n",
       "div.sk-item {\n",
       "  /* draw centered vertical line to link estimators */\n",
       "  background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));\n",
       "  background-size: 2px 100%;\n",
       "  background-repeat: no-repeat;\n",
       "  background-position: center center;\n",
       "}\n",
       "\n",
       "/* Parallel-specific style estimator block */\n",
       "\n",
       "#sk-container-id-2 div.sk-parallel-item::after {\n",
       "  content: \"\";\n",
       "  width: 100%;\n",
       "  border-bottom: 2px solid var(--sklearn-color-text-on-default-background);\n",
       "  flex-grow: 1;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-parallel {\n",
       "  display: flex;\n",
       "  align-items: stretch;\n",
       "  justify-content: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-parallel-item {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-parallel-item:first-child::after {\n",
       "  align-self: flex-end;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-parallel-item:last-child::after {\n",
       "  align-self: flex-start;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-parallel-item:only-child::after {\n",
       "  width: 0;\n",
       "}\n",
       "\n",
       "/* Serial-specific style estimator block */\n",
       "\n",
       "#sk-container-id-2 div.sk-serial {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "  align-items: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  padding-right: 1em;\n",
       "  padding-left: 1em;\n",
       "}\n",
       "\n",
       "\n",
       "/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is\n",
       "clickable and can be expanded/collapsed.\n",
       "- Pipeline and ColumnTransformer use this feature and define the default style\n",
       "- Estimators will overwrite some part of the style using the `sk-estimator` class\n",
       "*/\n",
       "\n",
       "/* Pipeline and ColumnTransformer style (default) */\n",
       "\n",
       "#sk-container-id-2 div.sk-toggleable {\n",
       "  /* Default theme specific background. It is overwritten whether we have a\n",
       "  specific estimator or a Pipeline/ColumnTransformer */\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "/* Toggleable label */\n",
       "#sk-container-id-2 label.sk-toggleable__label {\n",
       "  cursor: pointer;\n",
       "  display: flex;\n",
       "  width: 100%;\n",
       "  margin-bottom: 0;\n",
       "  padding: 0.5em;\n",
       "  box-sizing: border-box;\n",
       "  text-align: center;\n",
       "  align-items: start;\n",
       "  justify-content: space-between;\n",
       "  gap: 0.5em;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 label.sk-toggleable__label .caption {\n",
       "  font-size: 0.6rem;\n",
       "  font-weight: lighter;\n",
       "  color: var(--sklearn-color-text-muted);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 label.sk-toggleable__label-arrow:before {\n",
       "  /* Arrow on the left of the label */\n",
       "  content: \"▸\";\n",
       "  float: left;\n",
       "  margin-right: 0.25em;\n",
       "  color: var(--sklearn-color-icon);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 label.sk-toggleable__label-arrow:hover:before {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "/* Toggleable content - dropdown */\n",
       "\n",
       "#sk-container-id-2 div.sk-toggleable__content {\n",
       "  max-height: 0;\n",
       "  max-width: 0;\n",
       "  overflow: hidden;\n",
       "  text-align: left;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-toggleable__content.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-toggleable__content pre {\n",
       "  margin: 0.2em;\n",
       "  border-radius: 0.25em;\n",
       "  color: var(--sklearn-color-text);\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-toggleable__content.fitted pre {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 input.sk-toggleable__control:checked~div.sk-toggleable__content {\n",
       "  /* Expand drop-down */\n",
       "  max-height: 200px;\n",
       "  max-width: 100%;\n",
       "  overflow: auto;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {\n",
       "  content: \"▾\";\n",
       "}\n",
       "\n",
       "/* Pipeline/ColumnTransformer-specific style */\n",
       "\n",
       "#sk-container-id-2 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator-specific style */\n",
       "\n",
       "/* Colorize estimator box */\n",
       "#sk-container-id-2 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-label label.sk-toggleable__label,\n",
       "#sk-container-id-2 div.sk-label label {\n",
       "  /* The background is the default theme color */\n",
       "  color: var(--sklearn-color-text-on-default-background);\n",
       "}\n",
       "\n",
       "/* On hover, darken the color of the background */\n",
       "#sk-container-id-2 div.sk-label:hover label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "/* Label box, darken color on hover, fitted */\n",
       "#sk-container-id-2 div.sk-label.fitted:hover label.sk-toggleable__label.fitted {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator label */\n",
       "\n",
       "#sk-container-id-2 div.sk-label label {\n",
       "  font-family: monospace;\n",
       "  font-weight: bold;\n",
       "  display: inline-block;\n",
       "  line-height: 1.2em;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-label-container {\n",
       "  text-align: center;\n",
       "}\n",
       "\n",
       "/* Estimator-specific */\n",
       "#sk-container-id-2 div.sk-estimator {\n",
       "  font-family: monospace;\n",
       "  border: 1px dotted var(--sklearn-color-border-box);\n",
       "  border-radius: 0.25em;\n",
       "  box-sizing: border-box;\n",
       "  margin-bottom: 0.5em;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-estimator.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "/* on hover */\n",
       "#sk-container-id-2 div.sk-estimator:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-2 div.sk-estimator.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Specification for estimator info (e.g. \"i\" and \"?\") */\n",
       "\n",
       "/* Common style for \"i\" and \"?\" */\n",
       "\n",
       ".sk-estimator-doc-link,\n",
       "a:link.sk-estimator-doc-link,\n",
       "a:visited.sk-estimator-doc-link {\n",
       "  float: right;\n",
       "  font-size: smaller;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1em;\n",
       "  height: 1em;\n",
       "  width: 1em;\n",
       "  text-decoration: none !important;\n",
       "  margin-left: 0.5em;\n",
       "  text-align: center;\n",
       "  /* unfitted */\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted,\n",
       "a:link.sk-estimator-doc-link.fitted,\n",
       "a:visited.sk-estimator-doc-link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "div.sk-estimator:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "div.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "/* Span, style for the box shown on hovering the info icon */\n",
       ".sk-estimator-doc-link span {\n",
       "  display: none;\n",
       "  z-index: 9999;\n",
       "  position: relative;\n",
       "  font-weight: normal;\n",
       "  right: .2ex;\n",
       "  padding: .5ex;\n",
       "  margin: .5ex;\n",
       "  width: min-content;\n",
       "  min-width: 20ex;\n",
       "  max-width: 50ex;\n",
       "  color: var(--sklearn-color-text);\n",
       "  box-shadow: 2pt 2pt 4pt #999;\n",
       "  /* unfitted */\n",
       "  background: var(--sklearn-color-unfitted-level-0);\n",
       "  border: .5pt solid var(--sklearn-color-unfitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted span {\n",
       "  /* fitted */\n",
       "  background: var(--sklearn-color-fitted-level-0);\n",
       "  border: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link:hover span {\n",
       "  display: block;\n",
       "}\n",
       "\n",
       "/* \"?\"-specific style due to the `<a>` HTML tag */\n",
       "\n",
       "#sk-container-id-2 a.estimator_doc_link {\n",
       "  float: right;\n",
       "  font-size: 1rem;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1rem;\n",
       "  height: 1rem;\n",
       "  width: 1rem;\n",
       "  text-decoration: none;\n",
       "  /* unfitted */\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 a.estimator_doc_link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "#sk-container-id-2 a.estimator_doc_link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "#sk-container-id-2 a.estimator_doc_link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "</style><div id=\"sk-container-id-2\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>StandardScaler()</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-2\" type=\"checkbox\" checked><label for=\"sk-estimator-id-2\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow\"><div><div>StandardScaler</div></div><div><a class=\"sk-estimator-doc-link fitted\" rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.6/modules/generated/sklearn.preprocessing.StandardScaler.html\">?<span>Documentation for StandardScaler</span></a><span class=\"sk-estimator-doc-link fitted\">i<span>Fitted</span></span></div></label><div class=\"sk-toggleable__content fitted\"><pre>StandardScaler()</pre></div> </div></div></div></div>"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 14
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 构建数据集\n",
    "\n",
    "这里我们构建多输入的数据集，注意到数据集介绍里对每个特征定义如下：\n",
    "\n",
    "> **Attribute Information**:\n",
    "> - MedInc        median income in block group\n",
    "> - HouseAge      median house age in block group\n",
    "> - AveRooms      average number of rooms per household\n",
    "> - AveBedrms     average number of bedrooms per household\n",
    "> - Population    block group population\n",
    "> - AveOccup      average number of household members\n",
    "> - Latitude      block group latitude\n",
    "> - Longitude     block group longitude\n",
    "\n",
    "我们认为最后两维作为位置信息要单独处理，故制作数据集如下"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:40:54.289434Z",
     "start_time": "2025-01-17T13:40:54.282601Z"
    }
   },
   "source": [
    "from torch.utils.data import Dataset\n",
    "\n",
    "class HousingDataset(Dataset):\n",
    "    def __init__(self, mode='train'):\n",
    "        self.x, self.y = dataset_maps[mode]\n",
    "        self.x = torch.from_numpy(scaler.transform(self.x)).float()\n",
    "        self.y = torch.from_numpy(self.y).float().reshape(-1, 1)\n",
    "            \n",
    "    def __len__(self):\n",
    "        return len(self.x)\n",
    "    # 当要喂模型多输入时怎么做？这里的x是两个列表，第一个列表是(wide,deep)的特征，第二个列表是位置信息的特征\n",
    "    def __getitem__(self, idx):\n",
    "        return (self.x[idx],self.x[idx][-2:]), self.y[idx] #返回的是一个元组，元组里面是两个元素，第一个元素是一个元组，第二个元素是一个数。\n",
    "    \n",
    "    \n",
    "train_ds = HousingDataset(\"train\")\n",
    "valid_ds = HousingDataset(\"valid\")\n",
    "test_ds = HousingDataset(\"test\")"
   ],
   "outputs": [],
   "execution_count": 15
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:40:54.296739Z",
     "start_time": "2025-01-17T13:40:54.290438Z"
    }
   },
   "source": "train_ds[1]",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "((tensor([-0.2981,  0.3523, -0.1092, -0.2506, -0.0341, -0.0060,  1.0806, -1.0611]),\n",
       "  tensor([ 1.0806, -1.0611])),\n",
       " tensor([1.5140]))"
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 16
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### DataLoader"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:40:54.317085Z",
     "start_time": "2025-01-17T13:40:54.312765Z"
    }
   },
   "source": [
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "batch_size = 8\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=False)\n",
    "val_loader = DataLoader(valid_ds, batch_size=batch_size, shuffle=False)\n",
    "test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False)"
   ],
   "outputs": [],
   "execution_count": 17
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:40:54.335193Z",
     "start_time": "2025-01-17T13:40:54.330591Z"
    }
   },
   "source": [
    "#回归模型我们只需要1个数\n",
    "\n",
    "class WideDeep(nn.Module):\n",
    "    def __init__(self, input_dim=(8,2)):  # 输入维度为(8,2)，8是wide的特征维度，2是deep的特征维度\n",
    "        super().__init__()\n",
    "        self.deep = nn.Sequential(\n",
    "            nn.Linear(input_dim[1], 30),\n",
    "            nn.ReLU(),\n",
    "            nn.Linear(30, 30),\n",
    "            nn.ReLU()\n",
    "            )\n",
    "        # pytorch 需要自行计算输出输出维度\n",
    "        self.output_layer = nn.Linear(30 + input_dim[0], 1)\n",
    "        \n",
    "        # 初始化权重\n",
    "        self.init_weights()\n",
    "        \n",
    "    def init_weights(self):\n",
    "        \"\"\"使用 xavier 均匀分布来初始化全连接层的权重 W\"\"\"\n",
    "        for m in self.modules():\n",
    "            if isinstance(m, nn.Linear):\n",
    "                nn.init.xavier_uniform_(m.weight)\n",
    "                nn.init.zeros_(m.bias)\n",
    "        \n",
    "    def forward(self, x_wide, x_deep):\n",
    "        # x_deep.shape [batch size, 6]\n",
    "        deep_output = self.deep(x_deep)\n",
    "        # concat [batch size, 30] with [batch size 8]\n",
    "        concat = torch.cat([x_wide, deep_output], dim=1)\n",
    "        logits = self.output_layer(concat)\n",
    "        # logits.shape [batch size, 1]\n",
    "        return logits"
   ],
   "outputs": [],
   "execution_count": 18
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:40:54.350147Z",
     "start_time": "2025-01-17T13:40:54.344682Z"
    }
   },
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ],
   "outputs": [],
   "execution_count": 19
  },
  {
   "cell_type": "code",
   "source": [
    "model = WideDeep()\n",
    "model"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-17T13:40:54.383283Z",
     "start_time": "2025-01-17T13:40:54.377163Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "WideDeep(\n",
       "  (deep): Sequential(\n",
       "    (0): Linear(in_features=2, out_features=30, bias=True)\n",
       "    (1): ReLU()\n",
       "    (2): Linear(in_features=30, out_features=30, bias=True)\n",
       "    (3): ReLU()\n",
       "  )\n",
       "  (output_layer): Linear(in_features=38, out_features=1, bias=True)\n",
       ")"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 20
  },
  {
   "cell_type": "code",
   "source": [
    "for name, param in model.named_parameters():\n",
    "      print(name, param.shape)"
   ],
   "metadata": {
    "collapsed": false,
    "ExecuteTime": {
     "end_time": "2025-01-17T13:40:54.396680Z",
     "start_time": "2025-01-17T13:40:54.392787Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "deep.0.weight torch.Size([30, 2])\n",
      "deep.0.bias torch.Size([30])\n",
      "deep.2.weight torch.Size([30, 30])\n",
      "deep.2.bias torch.Size([30])\n",
      "output_layer.weight torch.Size([1, 38])\n",
      "output_layer.bias torch.Size([1])\n"
     ]
    }
   ],
   "execution_count": 21
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:40:54.411327Z",
     "start_time": "2025-01-17T13:40:54.407187Z"
    }
   },
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    for (datas_wide, datas_deep), labels in dataloader:\n",
    "        datas_deep = datas_deep.to(device)\n",
    "        datas_wide = datas_wide.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas_wide, datas_deep)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "    return np.mean(loss_list)\n"
   ],
   "outputs": [],
   "execution_count": 22
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:41:19.466586Z",
     "start_time": "2025-01-17T13:40:54.436103Z"
    }
   },
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for (datas_wide, datas_deep), labels in train_loader:#和上面自定义的dataset是一致的\n",
    "                datas_deep = datas_deep.to(device)\n",
    "                datas_wide = datas_wide.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits = model(datas_wide, datas_deep)\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    " \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "\n",
    "                    # 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(-val_loss)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 10\n",
    "\n",
    "\n",
    "\n",
    "# 1. 定义损失函数 采用MSE损失\n",
    "loss_fct = nn.MSELoss()\n",
    "# 2. 定义优化器 采用SGD\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.0)\n",
    "\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=5, min_delta=1e-3)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "  0%|          | 0/14520 [00:00<?, ?it/s]"
      ],
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "fc088f31183a4d33a17ed8a361d9f10f"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n",
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n",
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n",
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n",
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n",
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n"
     ]
    }
   ],
   "execution_count": 23
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:41:19.600328Z",
     "start_time": "2025-01-17T13:41:19.467591Z"
    }
   },
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    for idx, item in enumerate(train_df.columns):\n",
    "        plt.plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        plt.plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        plt.grid()\n",
    "        plt.legend()\n",
    "        # plt.xticks(range(0, train_df.index[-1], 10*sample_step), range(0, train_df.index[-1], 10*sample_step))\n",
    "        plt.xlabel(\"step\")\n",
    "\n",
    "        plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=500)  #横坐标是 steps"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ],
      "image/png": "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duzQRo0apZUvX147c+aM5g969+6tff7559r27du1zZs3a/369dNq1aqlpaWlOdd55JFHtKSkJG3RokXahg0btBtuuEHr0qWL8/Hc3FytRYsWWo8ePbTff/9dmz9/vpaYmKiNHTvWuc7Bgwe1mJgYbcyYMdrOnTu1999/XwsNDdUWLFig+dq6deu0OnXqaK1atdKefPLJgG73xYsXtdq1a2sPPvigtnbtWrV/P//8s7Z//37nOq+99ppWrlw5be7cudqWLVu0u+66S6tbt66WmZnpXKdPnz5a69attTVr1mgrVqzQGjRooA0bNsz5eHJysla1alXtvvvuU5+tb775RouOjtb+9a9/ab7wyiuvaJUqVdJ+/PFH7dChQ9qsWbO0uLg4bfLkyQHXbvkc/uMf/9Bmz54t0Zg2Z86cAo97q52//fab+qy//vrr6rP//PPPa+Hh4dq2bdu83u7Lly+rf6czZszQdu/era1evVrr2LGj1r59+wLbCLR2u5LHpW01atTQ3nnnHb9vd1EYsFyDfOgff/xx5+28vDz1gZg0aZLmj86ePas+9MuWLXP+Q5cPnXzBO+zatUutI//oHf9gQkJCtNOnTzvX+eijj7SEhAQtOztb3f7b3/6mNW/evMDfGjp0qAqYfCk1NVVr2LChtnDhQu2WW25xBiyB2u5nn31Wu+mmm4p83GazadWqVdPeeOMN533yWkRGRqovKSFfRvI6rF+/3rnOTz/9pFksFu3EiRPq9ocffqhVqFDB+To4/nbjxo01X7j99tu1P/zhDwXuGzRokPoCDuR2Fz6AebOdQ4YMUa+7q06dOml//vOfNaNd68Dt+kNF1jty5EjAt/v48ePaddddp4IN+cHiGrAEQrtdsUuoCDk5Odi4caNKqbqes0hur169Gv4oOTlZXVasWFFdSvskneraRkn51apVy9lGuZT0X9WqVZ3r9O7dW51Ia8eOHc51XLfhWMfXr5N0+UiXTuF9C9R2f//997j++usxePBg1YXVtm1bfPrpp87HDx06hNOnTxfYZzmHh3R1urZb0sayHQdZXz77a9euda7TtWtXREREFGi3dDdeunQJ3talSxcsWrQIe/fuVbe3bNmClStXom/fvgHd7sK82U6zffbdfddJ94i0NZDbbbPZ8MADD6ju6ubNm1/1eKC1mwFLEc6fP6/6xl0PWEJuy5eC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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 24
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 测试集"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-17T13:41:19.750376Z",
     "start_time": "2025-01-17T13:41:19.601333Z"
    }
   },
   "source": [
    "model.eval()\n",
    "loss = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\")"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.5339\n"
     ]
    }
   ],
   "execution_count": 25
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "pytorch",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
